C# の例外処理は、アプリの品質・安定性を左右する重要な要素です。 しかし、try/catch の書き方を誤ると、 例外が消える・原因が分からない・UIが落ちる・ログが残らない といった問題が発生します。
この記事でわかること
・例外の基本(Exception / InnerException)
・try/catch の正しい書き方
・再スロー(throw)の正しい使い方
・例外を握りつぶす危険性
・業務アプリで使う例外設計
・Serilog で例外ログを残す方法
・UIアプリ(WPF/WinForms)の未処理例外対策
・例外の基本(Exception / InnerException)
・try/catch の正しい書き方
・再スロー(throw)の正しい使い方
・例外を握りつぶす危険性
・業務アプリで使う例外設計
・Serilog で例外ログを残す方法
・UIアプリ(WPF/WinForms)の未処理例外対策
1. 例外の基本(Exception / InnerException)
■ 1-1. 例外は「エラーの詳細情報を持つオブジェクト」
- Message:エラー内容
- StackTrace:どこで発生したか
- InnerException:内部で起きた例外
■ 1-2. InnerException は必ず確認する
try
{
DoWork();
}
catch (Exception ex)
{
Console.WriteLine(ex.InnerException?.Message);
}
DB・API・ファイルI/O の例外は InnerException に原因が書かれていることが多いです。
2. try/catch の正しい書き方
■ 2-1. 最小限の範囲で try/catch を書く
// 悪い例:広すぎる
try
{
A();
B();
C();
}
catch { }
// 良い例:失敗しやすい箇所だけ
A();
try
{
B();
}
catch (Exception ex)
{
Log.Error(ex, "Bでエラー");
}
C();
try/catch は「失敗しやすい箇所」に限定するのが鉄則。
■ 2-2. 例外を握りつぶさない
// 悪い例(絶対NG)
catch (Exception)
{
}
ログも出さずに例外を消すと、原因が永遠に分からなくなります。
3. 再スロー(throw)の正しい使い方
■ 3-1. throw ex; は NG(スタックトレースが消える)
// 悪い例
catch (Exception ex)
{
throw ex; // スタックトレースがリセットされる
}
■ 3-2. 正しい再スロー
catch
{
throw; // 元の例外情報を保持したまま再スロー
}
再スローは throw; 一択。
4. 業務アプリでよくある例外処理パターン
■ 4-1. 入力チェック → 業務例外
if (amount <= 0)
throw new ArgumentException("金額は1以上を指定してください");
■ 4-2. DBエラー → ログを残して上位に投げる
try
{
await _repo.SaveAsync(order);
}
catch (Exception ex)
{
Log.Error(ex, "注文保存に失敗");
throw; // UI層に通知
}
■ 4-3. UI層でユーザー向けメッセージに変換
try
{
await _service.SaveOrderAsync(order);
}
catch (Exception ex)
{
MessageBox.Show("保存に失敗しました。ログを確認してください。");
}
「ログは技術者向け」「メッセージはユーザー向け」で分けるのが正解。
5. Serilog で例外ログを残す
■ 5-1. 例外ログの基本
try
{
DoWork();
}
catch (Exception ex)
{
Log.Error(ex, "処理中にエラー");
}
Serilog は例外オブジェクトを渡すだけで、 スタックトレースまで自動で記録してくれます。
■ 5-2. 未処理例外をキャッチ(WPF)
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
{
Log.Fatal((Exception)e.ExceptionObject, "未処理例外");
};
業務アプリでは必須の設定。
6. finally の正しい使い方
■ 6-1. リソース解放に使う
try
{
conn.Open();
DoWork();
}
finally
{
conn.Close();
}
finally は「成功しても失敗しても必ず実行される」ブロック。
7. async/await と例外処理の落とし穴
■ 7-1. async void は例外が消える
public async void Save()
{
throw new Exception("例外"); // 捕まらない
}
■ 7-2. async Task にする
public async Task SaveAsync()
{
throw new Exception("例外"); // 呼び出し元で捕まる
}
■ 7-3. Task.WhenAll の例外は AggregateException
await Task.WhenAll(task1, task2); // 複数例外に注意
8. 例外を使うべきでないケース
- 通常の条件分岐(if で十分)
- 存在チェック(Any を使う)
- ループの終了
例外は「異常系」にのみ使う。
9. 業務アプリ向けベストプラクティス
- try/catch は最小限の範囲にする
- 例外を握りつぶさない(ログ必須)
- 再スローは throw; を使う
- UI層ではユーザー向けメッセージに変換
- 未処理例外をキャッチしてログに残す
- async void はイベント以外禁止
- Serilog で例外ログを構造化して残す
まとめ:例外処理は“ログ × 再スロー × UI通知”の3点セットで設計する
- 例外は消さずにログへ
- throw ex; はNG、throw; が正解
- UI層ではユーザー向けメッセージに変換
- 未処理例外をキャッチしてアプリを安定化
「原因が分からないエラーが出る」「ログに何も残らない」 という現場の悩みは、この記事のパターンを押さえるだけで劇的に改善します。 あなたのプロジェクトに合わせて、最適な例外処理設計を組み立ててみてください。